Esse artigo é introdutório para a linguagem Assembly, ele não tem interesse em aprofundar-se. Além do básico, também terá as principais ferramentas para manipular essa linguagem.
Para entender o que é essa linguagem de programação, é primeiro necessário entendermos um pouco do funcionamento dos computadores, especificamente a parte considerada o "cérebro" do computador, a CPU.
Também conhecida como processador, é a parte responsável por fazer diversas operações lógicas, aritméticas, processamento de dados e executar o código de máquina de um programa de computador (que é a parte que mais nos interessa).
O código de máquina é um conjunto de instruções definidas por sua arquitetura, denominada como ISA (Instruction Set Archtecture), e elas são comumente representadas em hexadecimal como mostra o exemplo:
31 c0
b8 28 23 00 00
50
bb 10 90 12 76
ff d3
Do ponto de vista humano, entender instruções em código de máquina é muito difícil. Por isso os manuais da ISA, para facilitar a criação, simplificam o entendimento das instruções se referindo à elas em formato de texto. Esse formato é conhecido como notação mnemônico.
31 c0 XOR EAX, EAX
b8 28 23 00 00 MOV EAX, 0X2328
50 PUSH EAX
bb 10 90 12 76 MOV EBX, 0X76129010
ff d3 CALL EBX
Sendo essa notação usada para montar o código de máquina, o que a gente conhece atualmente como "linguagem Assembly" (Assembly remete a assembler que se traduz em montagem). Na verdade não existe uma única linguagem mas sim cada ISA tem uma linguagem Assembly diferente.
Anteriormente, os programadores escreviam seus códigos usando a notação em texto e depois manualmente convertiam para código de máquina. Porém, atualmente existem softwares que fazem esse processo automaticamente, chamados de assemblers.
Existem algumas arquiteturas diferentes, e entre elas existem diferenças significativas. As arquiteturas mais conhecidas são a x86 e x86-64. Dentre as diferenças existentes destaca-se as seguintes:
As arquiteturas se diferem no tamanho da capacidade padrão de dados. Como exemplo, x86 é compatível com 32 bits e o x86-64 com 64 bits. E ainda, sistemas 64 bits permitem operar com diferentes tamanhos de dados e acessar quantidades maiores de memória.
O número de registradores (pequenas unidades de armazenamento rápido) variam. Sendo que, arquiteturas x86 têm menos registradores em comparação com x86-64.
A stack ou pilha em português, é um espaço continuo de memória que os programas usam para manipular dados. E ainda, é utilizada para a "comunicação" durante chamadas e retornos de funções.
A stack ainda tem o comportamento LIFO (Last In First Out, em português Ultimo que entra é o primeiro a sair), como o nome diz replicando a característica de uma pilha. Logo, vale citar que quando inserindo informações na stack é necessário levar em consideração essa condição e inverter a ordem das informações.
E existe ainda alguns endereços fundamentais como:
db
que serve pra já associar valores no correspondente local do arquivo binário de saída (Define valor à um endereço)..
antes do nome.Existem duas sintaxes diferentes para programar em Assembly (Intel ou AT&T).
A sintaxe da Intel é no seguinte formato: MOV EAX, 3
Já a sintaxe AT&T: MOV $0X3, %EAX
O artigo será escrito usando a sintaxe Intel.
Instrução | Função |
---|---|
MOV | Move |
ADD | Adiciona |
SUB | Subtrai |
INC | Incrementa |
DEC | Descrementa |
CALL | Chama |
JMP | Salta |
JNE | Salta se não for igual |
CMP | Compara |
PUSH | Coloca na Stack (Topo) |
POP | Retira da Stack (Topo) |
NOP | No Operation (\x90) |
INT3 | Interrupção (Breakpoint) |
XOR | Instruções Lógicas |
MOV EAX, 3 # Coloca o valor 3 no endereço de memória EAX
MOV EAX, [ESP] # Coloca o conteúdo do topo da Stack para EAX
XOR EAX, EAX # Zera o valor em EAX
NOP # Não faz nada
PUSH 0x3 # Envia o valor 3 em Hex para o topo da Stack
POP EAX # Coloca o valor do topo da Stack para EAX
Para transformar o código em Assembly para um arquivo executável é necessário fazer dois procedimentos, sendo eles o assembler e "linkagem"
.obj
ou .o
.Quando desenvolvendo em Assembly para Windows é necessário seguir alguns processos. Primeiramente, é fundamental ter em mãos o site da Microsoft onde terá todas as funções que serão referenciadas e os arquivos que devem ser linkados https://learn.microsoft.com/en-us/windows/win32/api/
Como exemplo temos o seguinte código usado para criar uma caixa exibindo uma mensagem:
extern _MessageBoxA ; declarando a funcao externa que sera usada
global main
section .data ; espaco dedicado a declarar variaveis
msg db "Mensagem teste",0 ; db stands for declarative byte
titulo db "Titulo da caixa",0 ; ,0 serve como uma quebra de linha na secao de declaracao de variaveis
section .text
main:
PUSH 0
PUSH titulo
PUSH msg
PUSH 0
CALL _MessageBoxA
Onde temos a parte superior onde definimos que será usada uma função externa _MessageBoxA
usando o comando extern
, e logo após temos a definições das variáveis que serão usadas.
E acompanhando a sintaxe provida pelo site temos que está em C++:
int MessageBox(
[in, optional] HWND hWnd, # Um valor handle que recebe o valor 0
[in, optional] LPCTSTR lpText, # O valor que estará no texto
[in, optional] LPCTSTR lpCaption, # O título da caixa
[in] UINT uType # O tipo da caixa (Olhar o site para ver cada exemplo)
);
E considerando que na stack todos os comandos terão que ser enviados ao contrário temos a ordem:
main:
PUSH 0 # Valor do tipo
PUSH titulo # Texto do título
PUSH msg # Texto da caixa
PUSH 0 # Valor do handle
CALL _MessageBoxA
Por fim chamando o espaço da memória onde está a função da caixa de mensagem.
Quando estamos compilando o código no Windows usaremos os nasm e o golink da seguinte forma:
nasm -f win32 projeto.asm
golink /entry bloco principal de codigo projeto.obj arquivo a ser linkado ao código
Seguindo o exemplo mostrado acima usaremos os seguintes comandos e seguindo as necessidades do site da Microsoft:
nasm -f win32 caixa.asm
golink /entry main caixa.obj User32.dll
Assim teremos o arquivo .exe
funcional.
Para desenvolver em linux o processo é bem diferente se comparado com o Windows. Primeiramente é fundamental conhecer a ferramenta man, que é um manual digital onde podemos ver sobre as funções e ferramentas do sistema. Essa ferramenta é fundamental pois através dela teremos acesso ao manual ISA do nosso sistema.
E no linux nos usaremos as chamadas syscall
para trazer as funcionalidades ao nosso código. Então para começar devemos fazer o seguinte comando: man syscall
E apesar de todo o manual ser muito útil, o que mais nos interessa são as tabelas Architecture calling conventions
, onde veremos qual é a instrução que fará a chamada da função do sistema, e a tabela usada para passar aos argumentos. Para praticidade irei colocar aqui somente as que nos interessa, porém vale sempre usar o man como referência.
Arch/ABI Instruction System Ret Ret Error Notes
───────────────────────────────────────────────────────────────────
i386 int $0x80 eax eax edx -
x86-64 syscall rax rax rdx - 5
───────────────────────────────────────────────────────────────────
Arch/ABI arg1 arg2 arg3 arg4 arg5 arg6 arg7 Notes
──────────────────────────────────────────────────────────────
i386 ebx ecx edx esi edi ebp -
x86-64 rdi rsi rdx r10 r8 r9 -
──────────────────────────────────────────────────────────────
Primeiro iremos usar a arquitetura x86 ou como está escrito i386. Teremos que abrir o arquivo que tem o código que identifica cada syscall, que fica na pasta asm
em algum lugar do endereço /usr/include/
, no caso atual usaremos o arquivo unistd_32.h
. Usaremos os seguinte código que mostra o famoso "Hello World" como referência:
global main
section .data
teste: db 'Hello Word', 0xa
section .text
main:
MOV EAX, 4
MOV EBX, 1
MOV ECX, teste
MOV EDX, 11
INT 0X80
MOV EAX, 1
MOV EBX, 0
INT 0X80
Nesse código temos 2 funções sendo usadas, sendo a primeira a função write
que, de acordo com o arquivo citado acima, possui o código 4 que foi colocado no registrador EAX conforme citado na tabela. E olhando o manual dessa função por meio do comando man 2 write
, vemos que ela recebe 3 argumentos, sendo eles o FD (File Descriptor) que recebe o valor 1 para somente mostrar na tela, o texto que será usado e o tamanho do texto. Logo após temos a função exit
usada para fechar o código sem causar nenhum erro.
Agora para a arquitetura x86-64, perceberemos que não existe muitas diferenças. Primeiramente abriremos o arquivo unistd_64.h
e perceberemos que as funções possuem números diferentes. E ainda, levando em consideração que a syscall e os registradores são diferentes, teremos o código da seguinte forma:
global main
section .data
texto: db 'Hello World', 0xa
section .data
main:
MOV RAX, 1 ; Definindo a função write
MOV RD1, 1 ; Configurando como STDOUT
MOV RSI, texto ; Texto
MOV RDX, 11 ; Tamanho do texto
SYSCALL ; Chamada para executar a função write
MOV RAX, 60 ; Definindo a função exit
MOV RDI, 0 ; Código para sem erros
SYSCALL ; Chamada da função exit
Para compilar o código no linux iremos usar o nasm e o ld da seguinte forma:
nasm -f elf32 arquivo.asm # Para a arquitetura x86
nasm -f elf64 arquivo.asm # Para a arquitetura x86-64
ld --entry -m elf_i386 arquivo.o -o arquivo # Para arquitetura x86
ld --entry -m elf_x86_64 arquivo.o -o arquivo # Para arquitetura x86-64
Debuggers são ferramentas utilizadas para fazer analises nos softwares após sua compilação. Eles podem ser usado tanto para fazer uma engenharia reversa, quanto para explorar falhas como Buffer Overflow dentre outras funções. Dentre as opções disponíveis recomendo o Immunity Debugger para o Windows, e o gdb para o Linux.
https://mentebinaria.gitbook.io/assembly
https://eximia.co/entendendo-a-stack-em-sua-forma-mais-primitiva-em-assembly/